package util;

import java.text.Collator;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import com.google.code.linkedinapi.client.LinkedInApiClientException;
import com.google.code.linkedinapi.client.enumeration.ProfileField;
import com.google.code.linkedinapi.client.enumeration.SearchParameter;
import com.google.code.linkedinapi.schema.DateOfBirth;
import com.google.code.linkedinapi.schema.Education;
import com.google.code.linkedinapi.schema.People;
import com.google.code.linkedinapi.schema.Person;

import domain.LinkedInProfile;
import domain.Profile;
import domain.User;

public class LinkedInMatchHelper implements MatchHelper {
	private volatile static LinkedInMatchHelper linkedInMatchHelper = new LinkedInMatchHelper();
	
	private final String itbaName = "Instituto Tecnologico de Buenos Aires";
	private Map<User, List<Person>> conflicts = new HashMap<User, List<Person>>();
	private final List<Profile> matchedProfiles = new ArrayList<Profile>();
	private final List<Profile> conflictedProfiles = new ArrayList<Profile>();
	private final List<Profile> unmatchedProfiles = new ArrayList<Profile>();
	
	public LinkedInMatchHelper linkedInMatchHelper() {
		return linkedInMatchHelper;
	}
	
	/* (non-Javadoc)
	 * @see util.MatchHelper#getConflicts()
	 */
	public Map<User, List<Person>> getConflicts() {
		return conflicts;
	}

	/* (non-Javadoc)
	 * @see util.MatchHelper#getMatchedProfiles()
	 */
	@Override
	public List<Profile> getMatchedProfiles() {
		return matchedProfiles;
	}

	/* (non-Javadoc)
	 * @see util.MatchHelper#getConflictedProfiles()
	 */
	@Override
	public List<Profile> getConflictedProfiles() {
		return conflictedProfiles;
	}

	/* (non-Javadoc)
	 * @see util.MatchHelper#getUnmatchedProfiles()
	 */
	@Override
	public List<Profile> getUnmatchedProfiles() {
		return unmatchedProfiles;
	}
		
	/* (non-Javadoc)
	 * @see util.MatchHelper#match(com.google.code.linkedinapi.client.LinkedInApiClient, java.util.List)
	 */
	@Override
	public void match(ApiClientBuilder client, List<User> users) {
		TreeMap <Integer, List<Person>> ranking;
		int limit = 500;
		int i = 0;
		boolean batchFinalized = false;
		boolean tryMatchingIfError = false;
		if (users != null) {
			conflicts.clear();
			matchedProfiles.clear();
			unmatchedProfiles.clear();
			conflictedProfiles.clear();
			for (int j=0; j<users.size(); j++) {
				User user = users.get(j);
				try {
					// If a linkedinprofile already exists for that user, it updates it, otherwise
					// gets all candidates from linkedin, and process them
					if (((user.getProfile().getLinkedInProfile() == null || user.getProfile().getLinkedInProfile().getFirstName() == null) &&
							!user.getProfile().getLinkedInProfile().isBatchUpdated()) || tryMatchingIfError) {
						// Returns a map with all matched linkedin profiles with corresponding
						// match "score" which indicates best candidate to import. There may be cases
						// where two candidates has same score, or no candidate is found.
						ranking = matchSingle(client, user);
						if (!(ranking.isEmpty() || hasConflicts(ranking))) {
							// Candidate imported.
							importCandidate(ranking, user, matchedProfiles);
						} else if (!ranking.isEmpty()){
							// Temporary stores for each user, the list of matched profiles
							conflicts.put(user, ranking.lastEntry().getValue());
							conflictedProfiles.add(user.getProfile());
						} else {
							unmatchedProfiles.add(user.getProfile());
						}
					} else {
						updateMatch(client, user, user.getProfile().getLinkedInProfile().getLinkedinId());
						matchedProfiles.add(user.getProfile());
					}
					i++;
					batchFinalized = users.indexOf(user) == users.size() -1;
					if (i >= limit) {
						break;
					}
					tryMatchingIfError = false;
				} catch (Exception e) {
					if (e.getClass().equals(LinkedInApiClientException.class) && !tryMatchingIfError) {
						tryMatchingIfError = true;
						j--;
					}
				}
			}
		}
		if (batchFinalized) {
			for (User user : users) {
				if (user.getProfile() != null && user.getProfile().getLinkedInProfile() != null) {
					user.getProfile().getLinkedInProfile().setBatchUpdated(false);
				}
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see util.MatchHelper#importCandidate(domain.User, java.lang.String)
	 */
	@Override
	public boolean resolveCandidate(User user, String linkedInId) {
		boolean imported = false;
		for (Person p : conflicts.get(user)) {
			if (p.getId().equals(linkedInId)) {
				// Creates new or updates alumni user linkedin profile
				if (user.getProfile().getLinkedInProfile() == null) {
					doImport(user, p);
					imported = true;
				} else {
					doUpdate(user, p);
					imported = true;
				}
			} 
			if (imported) {
				conflictedProfiles.remove(conflictedProfiles.indexOf(user.getProfile()));
				conflicts.remove(user);
				break;
			}
		}
		return imported;
	}
	
	private void updateMatch(ApiClientBuilder builder, User user, String linkedInId) {
		Set<ProfileField> profileFieldsAvailable = ProfileField.valuesForConnections();
		Person p = builder.getLinkedInApiClient().getProfileById(linkedInId, profileFieldsAvailable);
		SimpleDateFormat formatObject = new SimpleDateFormat("dd/MM/yyyy");
		@SuppressWarnings("unused")
		Date last_info_date, current_info_date;
		try {
			current_info_date = formatObject.parse(user.getProfile().getLinkedInProfile().getLastStatusUpdate());
			last_info_date = new Date((long)p.getCurrentStatusTimestamp()*1000);
		} catch (ParseException e) {
			doUpdate(user, p);
		} catch (NullPointerException e) {
			doUpdate(user, p);
		}
	}
	
	private TreeMap <Integer, List<Person>> matchSingle(ApiClientBuilder client, User user) {
		Map<SearchParameter, String> searchParameters = new EnumMap<SearchParameter, String>(SearchParameter.class);
		Set<ProfileField> profileFieldsAvailable = ProfileField.valuesForConnections();
		searchParameters.put(SearchParameter.FIRST_NAME, user.getName());
		searchParameters.put(SearchParameter.LAST_NAME, user.getLastName());
		// TODO: aca deberia haber un try/catch que si no acepta los
		// tokens, se los pida de nuevo.
		// (fijate que esto esta implementado en connectLinkedin, pero
		// al reves)
		People people = client.getLinkedInApiClient().searchPeople(searchParameters, profileFieldsAvailable);
		return processLinkedInProfiles(user, people.getPersonList());
	}
	
	private TreeMap<Integer, List<Person>> processLinkedInProfiles(User user, List<Person> people) {
		//scorear por fecha de nacimiento??
		// la libreria no soporta buscar por fecha nacimiento... seria tactico
		TreeMap<Integer, List<Person>> analyzedLinkedInProfiles = new TreeMap<Integer, List<Person>>();
		Locale l = new Locale("ES");
		Collator collator = Collator.getInstance(l);
		collator.setStrength(Collator.PRIMARY);
		Calendar cal = Calendar.getInstance();
		DateOfBirth linkedInDateOfBirth = null;
		int score = 0;
		cal.setTime(user.getProfile().getBirthdate());
		if (people != null) {
			for (Person p : people) {
				if (p.getEducations() != null) {
					for (Education e : p.getEducations().getEducationList()) {
						// TODO: ontologias aca?... inaplicable
						if (collator.compare(e.getSchoolName(), itbaName) == 0) {
							score++;
						}
					}
				}
				linkedInDateOfBirth = p.getDateOfBirth();
				// if matches date of birth, score increases!
				if (linkedInDateOfBirth != null) {
					if (cal.get(Calendar.DAY_OF_MONTH) == linkedInDateOfBirth.getDay() 
						&& cal.get(Calendar.MONTH) == linkedInDateOfBirth.getMonth() &&
						cal.get(Calendar.YEAR) == linkedInDateOfBirth.getYear()) {
						score++;
					}
				}
				if (analyzedLinkedInProfiles.get(score) == null) {
					analyzedLinkedInProfiles.put(score, new ArrayList<Person>());
				}
				analyzedLinkedInProfiles.get(score).add(p);
				score = 0;
			}
		} 
		return analyzedLinkedInProfiles;
	}
	
	private boolean hasConflicts(TreeMap<Integer, List<Person>> matchedProfiles) {
		return matchedProfiles.lastEntry().getValue().size() > 1;
	}
	
	private void importCandidate(TreeMap<Integer, List<Person>> ranking, User candidate, 
			List<Profile> matchedProfiles) {
		doImport(candidate, ranking.lastEntry().getValue().get(0));
		// add result to list of matched candidates for info purposes
		matchedProfiles.add(candidate.getProfile());
		return;
	}
	
	private void doImport(User candidate, Person p) {
		LinkedInProfile profile = candidate.getProfile().getLinkedInProfile();
		if (profile == null) {
			profile = new LinkedInProfile();
			profile.setImportedProfile(p);
			candidate.getProfile().setLinkedInProfile(profile);
		} else {
			profile.setImportedProfile(p);
		}
		profile.setBatchUpdated(true);
	}
	
	private void doUpdate(User candidate, Person p) {
		candidate.getProfile().getLinkedInProfile().setImportedProfile(p);
		candidate.getProfile().getLinkedInProfile().setBatchUpdated(true);
	}

}
